﻿using System;
using System.Data.Entity;
using System.Linq;
using Shell.PocoDot.Configurations;

namespace Shell.PocoDot
{
    public class EfRepository : IRepository
    {
        private bool disposed;

        public EfRepository(RelationConfiguration config)
        {
            this.RelationConfiguration = config;
            this.InitializeNames();
            this.Initialize();
        }

        public RelationConfiguration RelationConfiguration
        {
            get;
            private set;
        }

        protected virtual DbContext DataContext
        {
            get;
            private set;
        }

        protected virtual DbSet<Relation> Data
        {
            get;
            private set;
        }

        protected string MainName { get; set; }

        protected string RelatedName { get; set; }

        public void Bind(Guid mainKey, Guid relatedKey)
        {
            var data = this.WhereKeys(mainKey, relatedKey);
            var existed = data.Any(r => r.MainName == this.MainName
                                     && r.RelatedName == this.RelatedName);
            if (existed)
            {
                var rconfig = this.RelationConfiguration;
                var main = rconfig.MainPocoDot;
                var related = rconfig.RelatedPocoDot;
                var errormsg = SR.Get("Error.RelationExists",
                    main.Name, related.Name, rconfig.Relationship, rconfig.ReversedRelationship);
                throw new InvalidOperationException(errormsg);
            }
            var newr = this.Data.Create();
            newr.Id = Guid.NewGuid();
            this.SetMainKey(newr, mainKey);
            this.SetRelatedKey(newr, relatedKey);
            newr.MainName = this.MainName;
            newr.RelatedName = this.RelatedName;
            this.Data.Add(newr);
        }

        public void Release(Guid mainKey, Guid relatedKey)
        {
            var data = this.WhereKeys(mainKey, relatedKey);
            var existed = data.SingleOrDefault(r => r.MainName == this.MainName
                                                 && r.RelatedName == this.RelatedName);
            if (existed == null)
            {
                var rconfig = this.RelationConfiguration;
                var main = rconfig.MainPocoDot;
                var related = rconfig.RelatedPocoDot;
                var errormsg = SR.Get("Error.RelationNotExist",
                    main.Name, related.Name, rconfig.Relationship, rconfig.ReversedRelationship);
                throw new InvalidOperationException(errormsg);
            }
            this.Data.Remove(existed);
        }

        public bool TryRead(Guid mainKey, out Guid relatedKey)
        {
            var data = this.WhereMainKey(mainKey);
            var relation = data.SingleOrDefault(r => r.MainName == this.MainName
                                                  && r.RelatedName == this.RelatedName);
            if (relation == null)
            {
                relatedKey = Guid.Empty;
                return false;
            }
            else
            {
                relatedKey = this.GetRelatedKey(relation);
                return true;
            }
        }

        public Guid Read(Guid mainKey)
        {
            Guid relatedkey;
            if (this.TryRead(mainKey, out relatedkey))
            {
                return relatedkey;
            }
            var rconfig = this.RelationConfiguration;
            var main = rconfig.MainPocoDot;
            var related = rconfig.RelatedPocoDot;
            var errormsg = SR.Get("Error.RelationNotExist",
                main.Name, related.Name, rconfig.Relationship, rconfig.ReversedRelationship);
            throw new InvalidOperationException(errormsg);
        }

        public IQueryable<Guid> ReadAll(Guid mainKey)
        {
            var data = from r in this.WhereMainKey(mainKey)
                       where r.MainName == this.MainName
                       && r.RelatedName == this.RelatedName
                       select r;
            return this.GetRelatedKey(data);
        }

        public int Submit()
        {
            return this.DataContext.SaveChanges();
        }

        public void Rollback()
        {
            this.Initialize();
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Initialize()
        {
            if (this.DataContext != null)
            {
                this.DataContext.Dispose();
            }
            var pconfig = this.RelationConfiguration.ProviderConfiguration;
            var cfgName = pconfig.ConnectionStringOrName;
            var dbmodel = pconfig.Compile();
            this.DataContext = new DbContext(cfgName, dbmodel);
            if (pconfig.CreateIfNotExists)
            {
                this.DataContext.Database.CreateIfNotExists();
            }
            this.Data = this.DataContext.Set<Relation>();
        }

        protected virtual void InitializeNames()
        {
            this.MainName = this.RelationConfiguration.MainPocoDot.Name;
            this.RelatedName = this.RelationConfiguration.RelatedPocoDot.Name;
        }

        protected virtual IQueryable<Relation> WhereMainKey(Guid mainKey)
        {
            return from d in this.Data
                   where d.MainId == mainKey
                   select d;
        }

        protected virtual IQueryable<Relation> WhereKeys(Guid mainKey, Guid relatedKey)
        {
            return from d in this.Data
                   where d.MainId == mainKey && d.RelatedId == relatedKey
                   select d;
        }

        protected virtual void SetMainKey(Relation relation, Guid key)
        {
            relation.MainId = key;
        }

        protected virtual void SetRelatedKey(Relation relation, Guid key)
        {
            relation.RelatedId = key;
        }

        protected virtual Guid GetRelatedKey(Relation relation)
        {
            return relation.RelatedId;
        }

        protected virtual IQueryable<Guid> GetRelatedKey(IQueryable<Relation> relations)
        {
            return relations.Select(r => r.RelatedId);
        }

        private void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.DataContext != null)
                    {
                        this.DataContext.Dispose();
                        this.DataContext = null;
                    }
                    this.Data = null;
                    this.RelationConfiguration = null;
                }
            }
            this.disposed = true;
        }
    }
}
